﻿/*
    Unit tests for the "Promises, Promises" project.
    These tests largely reflect the Promises/A+ specification, but go further to cover the entire library.

    Author:     Mike McMahon
    Created:    September 5, 2013

    Version:    1.0
    Updated:    September 5, 2013

*/

/* License and copyright information (Ms-PL): http://promises.codeplex.com */

/// <reference path="Promises.js" />
/// <reference path="Scripts/qunit.js" />

QUnit.begin(function () {

    // Bring the Promises objects we're testing into scope.
    window.Promise = Promises.Promise;
    window.Deferred = Promises.Deferred;
    window.DeferredState = Promises.DeferredState;
});

//#region Existence

module('Existence');

test("Promise is defined", function () {
    ok(!!Promise, 'Promises are present.');
});

test("Deferred is defined", function () {
    ok(!!Deferred, 'Deferreds are present.');
});

//#endregion

//#region Promise/A+ Specification

module('Promise/A+ Specification');

//#region Section 2

test('2.1: Promise defines the "then" method', function () {

    // Instantiate a new Promise with an empty "then" method to ensure it's passed.
    var p = new Deferred().promise();
    ok(!!p.then, 'The "then" method is present.');
});

//#endregion

//#region Section 3.1: States

asyncTest('3.1.1.1a: When in a "pending" state, the promise may transition to either "fulfilled" or "rejected"', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        outerCompleted = false;

    var continuation = p.then(function (val) {
        ok(outerCompleted && (val === 3.14), '"onFulfilled" was executed with the fulfillment value of "' + val + '" after the Promise was resolved.');
        start();
    });

    d.fulfill(3.14);
    ok(true, 'The promise was fulfilled with the numeric value "3.14".');
    outerCompleted = true;
});

asyncTest('3.1.1.1b: When in a "pending" state, the promise may transition to either "fulfilled" or "rejected"', 2, function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var d = new Deferred(),
        p = d.promise(),
        outerCompleted = false;

    var continuation = p.then(null, function (val) {
        ok(outerCompleted && (val === 'Unhappiness'), '"onRejected" was executed with the rejection value of "' + val + '" after the Promise was resolved.');
        start();
    });

    d.reject('Unhappiness');
    ok(true, 'The promise was rejected with the value "Unhappiness".');
    outerCompleted = true;
});

asyncTest('3.1.2.1: When in a "fulfilled" state, the promise must not transition to any other state', 5, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        successCounts = 0,
        failureCounts = 0,
        data = 'Hello';

    // Add handlers to count the number of executions.
    var continuation = p.then(function (d) {
        successCounts++;
    }, function (d) {
        failureCounts++;
    });

    // Fulfill the promise.
    d.fulfill(data);
    ok(true, 'The promise was fulfilled with the value "' + data + '".');

    // Try to do other things.
    d.reject(8);
    d.fulfill('Not the right data');
    d.reject();
    ok(true, 'Synchronous attempts were made to alter the state of the Promise.');

    setTimeout(function () {
        d.fulfill('Not the right async data');
        d.reject('Async rejection');
        ok(true, 'Asynchronous attempts were made to alter the state of the Promise.');
    }, 100);

    setTimeout(function () {
        strictEqual(successCounts, 1, 'The "onFulfilled" handler was executed only once.');
        strictEqual(failureCounts, 0, 'The "onRejected" handler was never executed.');
        start();
    }, 1000);
});

asyncTest('3.1.2.2: When in a "fulfilled" state, the promise must have a value that must not change', 7, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        successCounts = 0,
        failureCounts = 0,
        data = 'Hello';

    // Add handlers to count the number of executions.
    var continuation = p.then(function (d) {
        strictEqual(data, d, 'The promise has the expected fulfillment value.');
        successCounts++;
    }, function (d) {
        failureCounts++;
    });

    // Fulfill the promise.
    d.fulfill(data);
    ok(true, 'The promise was fulfilled with the value "' + data + '".');

    // Try to do other things.
    d.reject(data);
    d.fulfill('Not the right data');
    d.reject();
    ok(true, 'Synchronous attempts were made to alter the state of the Promise.');

    // Perform some asynchronous attempts.
    setTimeout(function () {
        d.fulfill('Not the right async data');
        d.reject('Async rejection');
        ok(true, 'Asynchronous attempts were made to alter the state of the Promise.');
    }, 100);

    // Check the return value of the success handler.
    setTimeout(function () {
        // Wire-up another handler to check the fulfillment value.
        p.then(function (d) {
            strictEqual(data, d, 'The promise still has the expected fulfillment value.');
        });

    }, 300);

    setTimeout(function () {
        strictEqual(successCounts, 1, 'The "onFulfilled" handler was executed only once.');
        strictEqual(failureCounts, 0, 'The "onRejected" handler was never executed.');
        start();
    }, 1000);
});

asyncTest('3.1.3.1: When in a "rejected" state, the promise must not transition to any other state', 5, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        successCounts = 0,
        failureCounts = 0,
        data = 'Explanation';

    // Add handlers to count the number of executions.
    var continuation = p.then(function (d) {
        successCounts++;
    }, function (d) {
        failureCounts++;
    });

    // Reject the promise.
    d.reject(data);
    ok(true, 'The promise was rejected with the value "' + data + '".');

    // Try to do other things.
    d.reject(5);
    d.fulfill('Not the right data');
    d.reject();
    ok(true, 'Synchronous attempts were made to alter the state of the Promise.');

    setTimeout(function () {
        d.fulfill('Not the right async data');
        d.reject('Async rejection');
        ok(true, 'Asynchronous attempts were made to alter the state of the Promise.');
    }, 100);

    setTimeout(function () {
        strictEqual(successCounts, 0, 'The "onFulfilled" handler was never executed.');
        strictEqual(failureCounts, 1, 'The "onRejected" handler was executed only once.');
        start();
    }, 1000);
});

asyncTest('3.1.3.2: When in a "rejected" state, the promise must have a reason that must not change', 7, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        successCounts = 0,
        failureCounts = 0,
        data = 'I failed';

    // Add handlers to count the number of executions.
    var continuation = p.then(function (d) {
        successCounts++;
    }, function (d) {
        strictEqual(data, d, 'The promise has the expected reason value.');
        failureCounts++;
    });

    // Reject the promise.
    d.reject(data);
    ok(true, 'The promise was rejected with the value "' + data + '".');

    // Try to do other things.
    d.reject(data);
    d.fulfill('Not the right data');
    d.reject();
    ok(true, 'Synchronous attempts were made to alter the state of the Promise.');

    // Perform some asynchronous attempts.
    setTimeout(function () {
        d.fulfill('Not the right async data');
        d.reject('Async rejection');
        ok(true, 'Asynchronous attempts were made to alter the state of the Promise.');
    }, 100);

    // Check the return value of the success handler.
    setTimeout(function () {
        // Wire-up another handler to check the fulfillment value.
        p.then(null, function (d) {
            strictEqual(data, d, 'The promise still has the expected fulfillment value.');
        });

    }, 300);

    setTimeout(function () {
        strictEqual(successCounts, 0, 'The "onFulfilled" handler was never executed.');
        strictEqual(failureCounts, 1, 'The "onRejected" handler was executed only once.');
        start();
    }, 1000);
});

//#endregion

//#region Section 3.2: The "then" Method

//#region Section 3.2.1: Arguments

test('3.2.1: "then" accepts both an "onFulfilled" and an "onRejected" callback', function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var p = new Deferred().promise();
    var continuation = p.then(function () { }, function () { });
    ok(!!continuation, 'Callbacks accepted');
});

test('3.2.1: "then" accepts no parameters', function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var p = new Deferred().promise();
    var continuation = p.then();
    ok(!!continuation, 'No-parameter invocation accepted');
});

test('3.2.1.1: "then" ignores a non-function "onFulfilled" parameter', function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var p = new Deferred().promise();
    var continuation = p.then('invalid');
    ok(!!continuation, 'Non-function ignored');
});

test('3.2.1.2: "then" ignores a non-function "onRejected" parameter', function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var p = new Deferred().promise();
    var continuation = p.then(null, 'invalid');
    ok(!!continuation, 'Non-function ignored');
});

//#endregion

//#region Section 3.2.2: onFulfilled

test('3.2.2: "then" accepts an "onFulfilled" callback', function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var p = new Deferred().promise();
    var continuation = p.then(function () { });
    ok(!!continuation, 'Callback accepted');
});

asyncTest('3.2.2.1: "then" accepts an "onFulfilled" callback that is executed after the promise is fulfilled', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        outerCompleted = false;

    var continuation = p.then(function () {
        ok(outerCompleted, '"onFulfilled" was executed after the Promise was resolved.');
        start();
    });

    d.fulfill();
    ok(true, 'The promise was fulfilled.');
    outerCompleted = true;
});

asyncTest('3.2.2.1: "then" accepts an "onFulfilled" callback that is executed after the promise is fulfilled, with the fulfillment value as its first argument', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        outerCompleted = false;

    var continuation = p.then(function (val) {
        ok(outerCompleted && (val === 3.14), '"onFulfilled" was executed with the fulfillment value of "' + val + '" after the Promise was resolved.');
        start();
    });

    d.fulfill(3.14);
    ok(true, 'The promise was fulfilled with the numeric value "3.14".');
    outerCompleted = true;
});

asyncTest('3.2.2.2: "then" accepts an "onFulfilled" callback that is executed only once', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        executeCount = 0;

    var continuation = p.then(function (val) {
        executeCount++;
        ok(true, '"onFulfilled" was called.');
    });

    d.fulfill();

    setTimeout(function () {
        ok(executeCount === 1, '"onFulfilled" was executed only once.');
        start();
    }, 1000);

});

asyncTest('3.2.2.3: "then" accepts an "onFulfilled" callback that is not executed if "onRejected" is called', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        fulfilledCalled = false,
        rejectedCalled = false;

    var continuation = p.then(function (val) {
        fulfilledCalled = true;
    }, function () {
        rejectedCalled = true;
        ok(true, '"onRejected" was called.');
    });

    d.reject();

    setTimeout(function () {
        ok(!fulfilledCalled && rejectedCalled, '"onFulfilled" was not executed.');
        start();
    }, 1000);
});

//#endregion

//#region Section 3.2.3: onRejected

test('3.2.3: "then" accepts an "onRejected" callback', function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var p = new Deferred().promise();
    var continuation = p.then(null, function () { });
    ok(!!continuation, 'Callback accepted');
});

asyncTest('3.2.3.1: "then" accepts an "onRejected" callback that is executed after the promise is rejected', 2, function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var d = new Deferred(),
        p = d.promise(),
        outerCompleted = false;

    var continuation = p.then(null, function () {
        ok(outerCompleted, '"onRejected" was executed after the Promise was rejected.');
        start();
    });

    d.reject();
    ok(true, 'The promise was rejected.');
    outerCompleted = true;
});

asyncTest('3.2.3.1: "then" accepts an "onRejected" callback that is executed after the promise is rejected, with the reason value as its first argument', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        outerCompleted = false;

    var continuation = p.then(null, function (val) {
        ok(outerCompleted && (val === 3.14), '"onRejected" was executed with the fulfillment value of "' + val + '" after the Promise was rejected.');
        start();
    });

    d.reject(3.14);
    ok(true, 'The promise was rejected with the numeric value "3.14".');
    outerCompleted = true;
});

asyncTest('3.2.3.2: "then" accepts an "onRejected" callback that is executed only once', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        executeCount = 0;

    var continuation = p.then(null, function (val) {
        executeCount++;
        ok(true, '"onRejected" was called.');
    });

    d.reject();

    setTimeout(function () {
        strictEqual(executeCount, 1, '"onRejected" was executed only once.');
        start();
    }, 1000);

});

asyncTest('3.2.3.3: "then" accepts an "onRejected" callback that is not executed if "onFulfilled" is called', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        fulfilledCalled = false,
        rejectedCalled = false;

    var continuation = p.then(function (val) {
        fulfilledCalled = true;
        ok(true, '"onFulfilled" was called.');
    }, function () {
        rejectedCalled = true;
    });

    d.fulfill();

    setTimeout(function () {
        ok(fulfilledCalled && !rejectedCalled, '"onRejected" was not executed.');
        start();
    }, 1000);
});

//#endregion

//#region Section 3.2.4: Return Timing

asyncTest('3.2.4a: "then" must return before "onRejected" is called', 2, function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var d = new Deferred(),
        p = d.promise(),
        thenReturned = false;

    // Reject the promise first, then wire-up to it.
    d.reject();

    var continuation = p.then(null, function () {
        ok(thenReturned, '"onRejected" was executed after the "then" method returned.');
        start();
    });

    thenReturned = true;
    ok(thenReturned, 'The call to "then" completed.');
});

asyncTest('3.2.4b: "then" must return before "onFulfilled" is called', 2, function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var d = new Deferred(),
        p = d.promise(),
        thenReturned = false;

    // Fulfill the promise first, then wire-up to it.
    d.fulfill();

    var continuation = p.then(function () {
        ok(thenReturned, '"onFulfilled" was executed after the "then" method returned.');
        start();
    });

    thenReturned = true;
    ok(thenReturned, 'The call to "then" completed.');
});

//#endregion

//#region Section 3.2.5: Multiple Invocations on the Same Instance

asyncTest('3.2.5.1: If / when the promise is fulfilled, respective "onFulfilled" callbacks must execute in the order of their originating calls to then', 6, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        p = d.promise(),
        orderCount = 1;

    // Invoke the "then" method several times for the instance, resolving it in the middle to mix things up.
    p.then(function () {
        strictEqual(orderCount, 1, 'First callback executed first.');
        orderCount++;
    });
    p.then(function () {
        strictEqual(orderCount, 2, 'Second callback executed second.');
        orderCount++;
    });
    p.then(function () {
        strictEqual(orderCount, 3, 'Third callback executed third.');
        orderCount++;
    });

    // Fulfill the Promise
    d.fulfill();

    p.then(function () {
        strictEqual(orderCount, 4, 'Fourth callback executed fourth.');
        orderCount++;
    });
    p.then(function () {
        strictEqual(orderCount, 5, 'Fifth callback executed fifth.');
        orderCount++;
    });
    p.then(function () {
        strictEqual(orderCount, 6, 'Sixth callback executed sixth.');
        orderCount++;
    });

    setTimeout(start, 1000);
});

asyncTest('3.2.5.2: If / when the promise is fulfilled, respective "onRejected" callbacks must execute in the order of their originating calls to then', 6, function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var d = new Deferred(),
        p = d.promise(),
        orderCount = 1;

    // Invoke the "then" method several times for the instance, resolving it in the middle to mix things up.
    p.then(null, function () {
        strictEqual(orderCount, 1, 'First callback executed first.');
        orderCount++;
    });
    p.then(null, function () {
        strictEqual(orderCount, 2, 'Second callback executed second.');
        orderCount++;
    });
    p.then(null, function () {
        strictEqual(orderCount, 3, 'Third callback executed third.');
        orderCount++;
    });

    // Reject the Promise
    d.reject();

    p.then(null, function () {
        strictEqual(orderCount, 4, 'Fourth callback executed fourth.');
        orderCount++;
    });
    p.then(null, function () {
        strictEqual(orderCount, 5, 'Fifth callback executed fifth.');
        orderCount++;
    });
    p.then(null, function () {
        strictEqual(orderCount, 6, 'Sixth callback executed sixth.');
        orderCount++;
    });

    setTimeout(start, 1000);
});

//#endregion

//#region Section 3.2.6: Calls to "then" Must Return a Promise

test('3.2.6: "then" returns a Promise', function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var p = new Deferred().promise();
    var continuation = p.then(function () { }, function () { });
    ok(!!continuation.then, 'Promise returned');
});

asyncTest('3.2.6.1a: If "onFulfilled" returns a value that is not a promise, the returned promise must be fulfilled with that value.', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        promise1Data = 'My wonderful data',
        promise2Data = 19872,
        promise1 = d.promise(),
        promise2 = promise1.then(function (d) {

            strictEqual(promise1Data, d, 'First fulfillment value is as expected.');

            // Return the new data.
            return promise2Data;
        });

    // Check the fulfillment value of the second promise by attaching a fulfillment handler.
    promise2.then(function (d) {

        strictEqual(promise2Data, d, 'Second fulfillment value is as expected.');
    });

    // Fulfill the promise first, then wire-up to it.
    d.fulfill(promise1Data);

    setTimeout(start, 500);
});

asyncTest('3.2.6.1b: If "onRejected" returns a value that is not a promise, the returned promise must be fulfilled with that value.', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        promise1Data = 'My rejection data',
        promise2Data = 1982.4,
        promise1 = d.promise(),
        promise2 = promise1.then(null, function (d) {

            strictEqual(promise1Data, d, 'First rejection value is as expected.');

            // Return the new data.
            return promise2Data;
        });

    // Check the fulfillment value of the second promise by attaching a fulfillment handler.
    promise2.then(function (d) {

        strictEqual(promise2Data, d, 'Second rejection value is as expected.');
    });

    // Reject the promise first, then wire-up to it.
    d.reject(promise1Data);

    setTimeout(start, 500);
});

asyncTest('3.2.6.2a: If "onFulfilled" throws an exception, the returned promise must be rejected with the thrown exception as the reason.', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        promise1Data = 'My wonderful data',
        exceptionMessage = 'I have erred',
        promise1 = d.promise(),
        promise2 = promise1.then(function (d) {

            strictEqual(promise1Data, d, 'First fulfillment value is as expected.');

            // Throw an exception.
            throw exceptionMessage;
        });

    // Check the fulfillment value of the second promise by attaching a fulfillment handler.
    promise2.then(null, function (d) {

        strictEqual(exceptionMessage, d, 'The exception was provided.');
    });

    // Fulfill the promise first, then wire-up to it.
    d.fulfill(promise1Data);

    setTimeout(start, 500);
});

asyncTest('3.2.6.2b: If "onRejected" throws an exception, the returned promise must be rejected with the thrown exception as the reason.', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        promise1Data = 'My wonderful data',
        exceptionMessage = 'I have erred',
        promise1 = d.promise(),
        promise2 = promise1.then(null, function (d) {

            strictEqual(promise1Data, d, 'First fulfillment value is as expected.');

            // Throw an exception.
            throw exceptionMessage;
        });

    // Check the rejection value of the second promise by attaching a fulfillment handler.
    promise2.then(null, function (d) {

        strictEqual(exceptionMessage, d, 'The exception was provided.');
    });

    // Reject the promise first, then wire-up to it.
    d.reject(promise1Data);

    setTimeout(start, 500);
});

asyncTest('3.2.6.3: If either "onFulfilled" or "onRejected" returns a promise (call it returnedPromise), the promise returned by "then" must assume the state of returnedPromise.', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        promise1Data = 'My wonderful data',
        innerPromiseData = 'I am inner data',
        exceptionMessage = 'I have erred',
        promise1 = d.promise(),
        promise2 = promise1.then(function (d) {

            strictEqual(promise1Data, d, 'First fulfillment value is as expected.');

            // Construct and return a Promise.
            var inner = new Deferred();

            // Complete the promise after a short delay.
            setTimeout(function () { inner.fulfill(innerPromiseData); }, 30);

            return inner.promise();
        }, function (d) {

            strictEqual(promise1Data, d, 'First rejection value is as expected.');

            // Construct and return a Promise.
            var inner = new Deferred();

            // Complete the promise after a short delay.
            setTimeout(function () { inner.reject(exceptionMessage); }, 30);

            return inner.promise();
        });

    // Check the rejection value of the second promise by attaching a fulfillment handler.
    promise2.then(function (d) {

        strictEqual(innerPromiseData, d, 'The inner Promise value was provided.');
    }, function (d) {

        strictEqual(exceptionMessage, d, 'The inner Promise reason was provided.');
    });

    // Reject the promise first, then wire-up to it.
    d.fulfill(promise1Data);

    setTimeout(start, 500);
});

asyncTest('3.2.6.3.1a: If returnedPromise is pending, the promise returned by "then" must remain pending until returnedPromise is fulfilled.', 1, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        innerComplete = false,
        promise1 = d.promise(),
        promise2 = promise1.then(function () {

            // Construct and return a new Promise.
            var inner = new Deferred();

            // Complete the promise after a short delay.
            setTimeout(function () {
                inner.fulfill();
                innerComplete = true;
            }, 30);

            return inner.promise();
        });

    // Check the rejection value of the second promise by attaching a fulfillment handler.
    promise2.then(function (d) {

        ok(innerComplete, 'The inner promise was fulfilled first.');
    });

    // Fulfill the promise.
    d.fulfill();

    setTimeout(start, 500);
});

asyncTest('3.2.6.3.1b: If returnedPromise is pending, the promise returned by "then" must remain pending until returnedPromise is rejected.', 1, function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var d = new Deferred(),
        innerComplete = false,
        promise1 = d.promise(),
        promise2 = promise1.then(null, function () {

            // Construct and return a new Promise.
            var inner = new Deferred();

            // Complete the promise after a short delay.
            setTimeout(function () {
                inner.reject();
                innerComplete = true;
            }, 30);

            return inner.promise();
        });

    // Check the rejection value of the second promise by attaching a fulfillment handler.
    promise2.then(null, function (d) {

        ok(innerComplete, 'The inner promise was rejected first.');
    });

    // Fulfill the promise.
    d.reject();

    setTimeout(start, 500);
});

asyncTest('3.2.6.3.1c: If returnedPromise never completes, the promise returned by "then" must never complete.', 2, function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var d = new Deferred(),
        innerComplete = false,
        outerComplete = false,
        promise1 = d.promise(),
        promise2 = promise1.then(null, function () {

            // Construct and return a new Promise.
            var inner = new Deferred();

            // Complete the promise after a short delay.
            inner.then(function () {
                innerComplete = true;
            });

            return inner.promise();
        });

    promise2.then(function () { outerComplete = true; }, function () { outerComplete = true; });

    // Fulfill the promise.
    d.reject();

    setTimeout(function () {
        ok(!innerComplete, 'The inner promise is still pending.');
        ok(!outerComplete, 'The outer promise is still pending.');
        start();
    }, 500);
});

asyncTest('3.2.6.3.2: When returnedPromise is fulfilled, the promise returned by "then" must be fulfilled with the same value.', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        fulfillmentValue = 793.4,
        promise1 = d.promise(),
        promise2 = promise1.then(function (d) {

            ok(!d, 'The original promise provided no value.');

            // Construct and return a new Promise.
            var inner = new Deferred();

            // Complete the promise after a short delay.
            setTimeout(function () {
                inner.fulfill(fulfillmentValue);
            }, 30);

            return inner.promise();
        });

    // Check the rejection value of the second promise by attaching a fulfillment handler.
    promise2.then(function (d) {

        strictEqual(fulfillmentValue, d, 'The fulfillment value of the inner promise was provided.');
    });

    // Fulfill the promise.
    d.fulfill();

    setTimeout(start, 500);
});

asyncTest('3.2.6.3.3: When returnedPromise is rejected, the promise returned by "then" must be rejected with the same reason.', 2, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        rejectionReason = 'I cannot work. Sad for me.',
        promise1 = d.promise(),
        promise2 = promise1.then(function (d) {

            ok(!d, 'The original promise was fulfilled and provided no value.');

            // Construct and return a new Promise.
            var inner = new Deferred();

            // Complete the promise after a short delay.
            setTimeout(function () {
                inner.reject(rejectionReason);
            }, 30);

            return inner.promise();
        });

    // Check the rejection value of the second promise by attaching a fulfillment handler.
    promise2.then(null, function (d) {

        strictEqual(rejectionReason, d, 'The rejection reason of the inner promise was provided.');
    });

    // Fulfill the promise.
    d.fulfill();

    setTimeout(start, 500);
});

asyncTest('3.2.6.4: If "onFulfilled" is not a function and the promise is fulfilled, the returned promise must be fulfilled with the same value.', 1, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        promise1Data = 'My delightful data',
        promise1 = d.promise(),
        promise2 = promise1.then(10);

    // Check the fulfillment value of the second promise by attaching a fulfillment handler.
    promise2.then(function (d) {

        strictEqual(promise1Data, d, 'Fulfillment value is the original value.');
    });

    // Fulfill the promise first, then wire-up to it.
    d.fulfill(promise1Data);

    setTimeout(start, 500);
});

asyncTest('3.2.6.5: If "onRejected" is not a function and the promise is rejected, the returned promise must be rejected with the same reason.', 1, function () {

    // Instantiate a new Promise and pass it a fulfillment callback.
    var d = new Deferred(),
        promise1Data = 'My rejection data',
        promise1 = d.promise(),
        promise2 = promise1.then(null, 'Howdy');

    // Check the fulfillment value of the second promise by attaching a fulfillment handler.
    promise2.then(null, function (d) {

        strictEqual(promise1Data, d, 'Fulfillment value is the original value.');
    });

    // Fulfill the promise first, then wire-up to it.
    d.reject(promise1Data);

    setTimeout(start, 500);
});

//#endregion

//#endregion

//#endregion

//#region Promise Enhancements

module('Promise Enhancements');

test('Promise has a static "never" instance', function () {
    ok(!!Promise.never, '"Promise.never" is present.');
});

test('Promise has a static "fulfilled" instance', function () {
    ok(!!Promise.fulfilled, '"Promise.fulfilled" is present.');
});

test('Promise has a static "rejected" instance', function () {
    ok(!!Promise.rejected, '"Promise.rejected" is present.');
});

test('Promise has a static "whenAll" method', function () {
    ok(!!Promise.whenAll, '"Promise.whenAll" is present.');
});

test('Promise has an "always" method', function () {
    var p = new Deferred().promise();
    ok(!!p.always, '"Promise.always" is present.');
});

asyncTest('"whenAll" is fulfilled when an empty array of promises is provided', 1, function () {

    var queuedCompletion;

    Promise.whenAll([]).then(function () {
        ok(true, 'Completed');

        clearInterval(queuedCompletion);
        start();
    });

    queuedCompletion = setTimeout(start, 100);
});

asyncTest('"whenAll" completes when all component promises complete', 15, function () {

    var deferreds = [],
        queuedCompletion;

    for (var i = 0; i < 15; i++) {
        deferreds.push(new Deferred());
    }
    var allDone = Promise.whenAll(deferreds);    // Mix up Deferreds and Promises for fun.

    allDone.then(function () {

        // Check each Deferred.
        for (var i = 0; i < deferreds.length; i++) {
            ok(deferreds[i].getState() === DeferredState.Fulfilled, 'Promise ' + i + ' was fulfilled.');
        }

        clearTimeout(queuedCompletion);
        start();
    });

    // Resolve the Deferreds at some interval.
    for (var i = 0; i < deferreds.length; i++) {
        (function (i) {
            var d = deferreds[i];

            setTimeout(function () {
                d.fulfill();
            }, i * 10 - i);
        }(i));
    }

    queuedCompletion = setTimeout(start, 1000);
});

asyncTest('"whenAll" is rejected when any component promise is rejected', 1, function () {

    var deferreds = [],
        queuedCompletion;

    for (var i = 0; i < 15; i++) {
        deferreds.push(new Deferred());
    }
    var allDone = Promise.whenAll(deferreds);    // Mix up Deferreds and Promises for fun.

    allDone.then(null, function () {

        ok(true, 'Rejection was detected.');

        clearTimeout(queuedCompletion);
        start();
    });

    // Resolve the Deferreds at some interval.
    for (var i = 0; i < deferreds.length; i++) {
        (function (i) {
            var d = deferreds[i];

            setTimeout(function () {
                if (i == 9) {
                    d.reject();
                }
                else {
                    d.fulfill();
                }
            }, i * 10 - i);
        }(i));
    }

    queuedCompletion = setTimeout(start, 1000);
});

test('"always" returns a Promise', function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var p = new Deferred().promise();
    var continuation = p.always(function () { });
    ok(!!continuation.then, 'Promise returned');
});

asyncTest('"always" executes the continuation method when fulfilled', 0, function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var d = new Deferred(),
        p = d.promise();
    var continuation = p.always(start);
    d.fulfill();
});

asyncTest('"always" executes the continuation method when rejected', 0, function () {

    // Instantiate a new Promise and pass it a rejection callback.
    var d = new Deferred(),
        p = d.promise();
    var continuation = p.always(start);
    d.reject();
});

//#endregion

//#region Conversion

module('Conversion');

//#region Method Existence

test("'Convert.fromSyncMethod' is defined", function () {
    ok(!!Promises.Convert.fromSyncMethod, "'Promise.Convert.fromSyncMethod' is present.");
});

test("'Convert.fromAsyncMethod' is defined", function () {
    ok(!!Promises.Convert.fromAsyncMethod, "'Promise.Convert.fromAsyncMethod' is present.");
});

test("'Convert.fromNodeAsyncMethod' is defined", function () {
    ok(!!Promises.Convert.fromNodeAsyncMethod, "'Promise.Convert.fromNodeAsyncMethod' is present.");
});

//#endregion

//#region Asynchronous Method Conversion

test("Calling 'Convert.fromAsyncMethod' returns a method", function () {
    ok(typeof (Promises.Convert.fromAsyncMethod(function () { })) === typeof (function () { }), " Calling 'Promise.Convert.fromAsyncMethod' produced a method.");
});

test("Calling 'Convert.fromAsyncMethod' returns a method that, when invoked, produces a Promise", function () {

    // Create a simple asynchronous operation and wrapper for it.
    function asyncOperation(callback) {

        setTimeout(function () {
            callback();
        }, 0);
    }

    function wrapper(fulfill, reject) {
        asyncOperation(fulfill);
    }

    var wrapped = Promises.Convert.fromAsyncMethod(wrapper, null);

    // We're fine if we have a Promise.
    ok(wrapped() instanceof Promises.Promise, "Invoking the PromiseFactory produces a Promise.");
});

asyncTest("Invoking the PromiseFactory produced by 'Convert.fromAsyncMethod' passes the proper arguments", 9, function () {

    // Create a simple asynchronous operation and wrapper for it.
    function asyncOperation(callback, myArg1, myArg2) {

        ok(arguments.length === 3, "All expected arguments are present.");

        setTimeout(function () {

            // Turn them into one value.
            callback(myArg1, myArg2);
        }, 0);
    }

    function wrapper(fulfill, reject) {
        asyncOperation(function (a1, a2) { fulfill({ a: a1, b: a2 }); }, arguments[2], arguments[3]);
    }

    function verifyResult(r, expectedA, expectedB) {
        ok(r.a === expectedA, "First argument was returned as expected.");
        ok(r.b === expectedB, "Second argument was returned as expected.");
    }

    // Invoke the factory a few times.
    var wrapped = Promises.Convert.fromAsyncMethod(wrapper, null),
        op2 = wrapped(3, 4).then(function (r) { verifyResult(r, 3, 4); }),
        op1 = wrapped(1, 2).then(function (r) { verifyResult(r, 1, 2); }),
        op3 = wrapped(5, 6).then(function (r) { verifyResult(r, 5, 6); });

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});

asyncTest("The invoked PromiseFactory produced by 'Convert.fromAsyncMethod' is run under the proper context", 3, function () {

    // Create a simple asynchronous operation and wrapper for it.
    function asyncOperation(callback, expectedContext) {

        ok(this === expectedContext, "The operation was executed under the expected context.");

        setTimeout(function () {

            callback();
        }, 0);
    }

    function wrapper(fulfill, reject) {
        asyncOperation.apply(this, [fulfill, arguments[2]]);
    }

    // Contexts
    var context1 = null,    // This should result in a context of the window...
        context2 = {},
        context3 = { a: "This is a string." };

    // Invoke the factory a few times.
    var op1 = Promises.Convert.fromAsyncMethod(wrapper, context1)(window),
        op2 = Promises.Convert.fromAsyncMethod(wrapper, context2)(context2),
        op3 = Promises.Convert.fromAsyncMethod(wrapper, context3)(context3);

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});

asyncTest("The invoked PromiseFactory produced by 'Convert.fromAsyncMethod' uses the proper 'this' context when no explicit context is provided and the factory is attached to a prototype.", 3, function () {

    // Create a simple asynchronous operation and wrapper for it.
    function asyncOperation(callback) {

        // Capture the value so that we don't need to do any additional "apply"-ing.
        var context = this;
        setTimeout(function () {

            callback(context.val);
        }, 0);
    }

    function wrapper(fulfill, reject) {
        asyncOperation.apply(this, [fulfill]);
    }

    function testObject(initialValue) {
        this.val = initialValue;
    }

    // Contexts
    var context1 = new testObject(1),
        context2 = new testObject('hello'),
        context3 = new testObject(true);

    // Create a wrapper that has no explicit context, attaching it to the prototype of the object.
    testObject.prototype.getValueAsync = Promises.Convert.fromAsyncMethod(wrapper);

    var op1 = context1.getValueAsync().then(function (v) { ok(v === 1, "The expected value was retrieved."); }),
        op2 = context2.getValueAsync().then(function (v) { ok(v === 'hello', "The expected value was retrieved."); }),
        op3 = context3.getValueAsync().then(function (v) { ok(v === true, "The expected value was retrieved."); });

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});

asyncTest("The invoked PromiseFactory produced by 'Convert.fromAsyncMethod' uses the proper 'this' context when no explicit context is provided and the factory is attached to arbitrary objects.", 3, function () {

    function wrapper(fulfill, reject) {
        // Capture the value so that we don't need to do any additional "apply"-ing.
        var context = this;
        setTimeout(function () {

            fulfill(context.v.toString());
        }, 0);
    }

    // Contexts
    var context1 = { v: 1 },
        context2 = { v: 'hello' },
        context3 = { v: true };

    // Create a wrapper that has no explicit context, attaching it to the objects.
    getValueAsync = Promises.Convert.fromAsyncMethod(wrapper);
    context1.getValueAsync = getValueAsync;
    context2.getValueAsync = getValueAsync;
    context3.getValueAsync = getValueAsync;

    var op1 = context1.getValueAsync().then(function (v) { ok(v === '1', "The expected value was retrieved."); }),
        op2 = context2.getValueAsync().then(function (v) { ok(v === 'hello', "The expected value was retrieved."); }),
        op3 = context3.getValueAsync().then(function (v) { ok(v === 'true', "The expected value was retrieved."); });

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});
//#endregion

//#region Synchronous Method Conversion

test("Calling 'Convert.fromSyncMethod' returns a method", function () {
    ok(typeof (Promises.Convert.fromSyncMethod(function () { })) === typeof (function () { }), " Calling 'Promise.Convert.fromSyncMethod' produced a method.");
});

test("Calling 'Convert.fromSyncMethod' returns a method that, when invoked, produces a Promise", function () {

    // Create a simple synchronous operation and wrapper for it.
    function syncOperation(returnValue) {

        return returnValue;
    }

    var wrapped = Promises.Convert.fromSyncMethod(syncOperation, null);

    // We're fine if we have a Promise.
    ok(wrapped() instanceof Promises.Promise, "Invoking the PromiseFactory produces a Promise.");
});

asyncTest("Invoking the PromiseFactory produced by 'Convert.fromSyncMethod' passes the proper arguments", 14, function () {

    // Create a simple synchronous operation to accept some arguments and return them.
    function syncOperation(argCount) {

        ok(arguments.length === (argCount + 1), "All " + arguments.length + " expected arguments are present.");

        return arguments;
    }

    function verifyResult(r) {

        ok(r.length === arguments.length, "The expected number of arguments (" + r.length + ") were returned.");

        var i = 1;
        while (arguments[i] !== undefined) {
            ok(r[i] === arguments[i], "Argument '" + r[i].toString() + "' was returned as expected.");
            i++;
        }
    }

    // Invoke the factory a few times.
    var wrapped = Promises.Convert.fromSyncMethod(syncOperation, null),
        op1 = wrapped(0).then(function (r) { verifyResult(r); }),
        op2 = wrapped(1, 'a').then(function (r) { verifyResult(r, 'a'); }),
        op3 = wrapped(2, 'b', true).then(function (r) { verifyResult(r, 'b', true); }),
        op4 = wrapped(3, 5, 'cat', window).then(function (r) { verifyResult(r, 5, 'cat', window); });

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3, op4]).then(start);
});

asyncTest("The invoked PromiseFactory produced by 'Convert.fromSyncMethod' is run under the proper context", 3, function () {

    // Create a simple asynchronous operation and wrapper for it.
    function syncOperation(expectedContext) {

        ok(this === expectedContext, "The operation was executed under the expected context.");
    }

    // Contexts
    var context1 = null,    // This should result in a context of the window...
        context2 = {},
        context3 = { a: "This is a string." };

    // Invoke the factory a few times.
    var op1 = Promises.Convert.fromSyncMethod(syncOperation, context1)(window),
        op2 = Promises.Convert.fromSyncMethod(syncOperation, context2)(context2),
        op3 = Promises.Convert.fromSyncMethod(syncOperation, context3)(context3);

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});

asyncTest("The invoked PromiseFactory produced by 'Convert.fromSyncMethod' uses the proper 'this' context when no explicit context is provided and the factory is attached to a prototype.", 3, function () {

    // Create a simple asynchronous operation and wrapper for it.
    function syncOperation() {

        // Capture the value so that we don't need to do any additional "apply"-ing.
        return this.val;
    }

    function testObject(initialValue) {
        this.val = initialValue;
    }

    // Contexts
    var context1 = new testObject(1),
        context2 = new testObject('hello'),
        context3 = new testObject(true);

    // Create a wrapper that has no explicit context, attaching it to the prototype of the object.
    testObject.prototype.getValueAsync = Promises.Convert.fromSyncMethod(syncOperation);

    var op1 = context1.getValueAsync().then(function (v) { ok(v === 1, "The expected value was retrieved."); }),
        op2 = context2.getValueAsync().then(function (v) { ok(v === 'hello', "The expected value was retrieved."); }),
        op3 = context3.getValueAsync().then(function (v) { ok(v === true, "The expected value was retrieved."); });

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});

asyncTest("The invoked PromiseFactory produced by 'Convert.fromSyncMethod' uses the proper 'this' context when no explicit context is provided and the factory is attached to arbitrary objects.", 3, function () {

    function syncMethod() {
        return this.v.toString();
    }

    // Contexts
    var context1 = { v: 1 },
        context2 = { v: 'hello' },
        context3 = { v: true };

    // Create a wrapper that has no explicit context, attaching it to the objects.
    getValueAsync = Promises.Convert.fromSyncMethod(syncMethod);
    context1.getValueAsync = getValueAsync;
    context2.getValueAsync = getValueAsync;
    context3.getValueAsync = getValueAsync;

    var op1 = context1.getValueAsync().then(function (v) { ok(v === '1', "The expected value was retrieved."); }),
        op2 = context2.getValueAsync().then(function (v) { ok(v === 'hello', "The expected value was retrieved."); }),
        op3 = context3.getValueAsync().then(function (v) { ok(v === 'true', "The expected value was retrieved."); });

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});

//#endregion

//#region Node.js Asynchronous Method Conversion

test("Calling 'Convert.fromNodeAsyncMethod' returns a method", function () {
    ok(typeof (Promises.Convert.fromNodeAsyncMethod(function () { })) === typeof (function () { }), " Calling 'Promise.Convert.fromNodeAsyncMethod' produced a method.");
});

test("Calling 'Convert.fromNodeAsyncMethod' returns a method that, when invoked, produces a Promise", function () {

    // Create a simple asynchronous operation and wrapper for it.
    function nodeAsyncOperation(callback) {

        setTimeout(function () {
            callback(null);
        }, 0);
    }

    var wrapped = Promises.Convert.fromNodeAsyncMethod(nodeAsyncOperation, null);

    // We're fine if we have a Promise.
    ok(wrapped() instanceof Promises.Promise, "Invoking the PromiseFactory produces a Promise.");
});

asyncTest("Invoking the PromiseFactory produced by 'Convert.fromNodeAsyncMethod' passes the proper arguments", 14, function () {

    // Create a simple synchronous operation to accept some arguments and return them.
    function nodeAsyncOperation(argCount) {

        // Remember the the callback at the end adds an argument.
        ok(arguments.length === (argCount + 2), "All " + arguments.length + " expected arguments are present.");
        var originalArguments = arguments;
        setTimeout(function () {
            originalArguments[originalArguments.length - 1](null, originalArguments)
        }, 0);
    }

    function verifyResult(r) {

        // Remember the the callback at the end adds an argument.
        ok(r.length === (arguments.length + 1), "The expected number of arguments (" + r.length + ") were returned.");

        var i = 1;
        while (arguments[i] !== undefined) {
            ok(r[i] === arguments[i], "Argument '" + r[i].toString() + "' was returned as expected.");
            i++;
        }
    }

    // Invoke the factory a few times.
    var wrapped = Promises.Convert.fromNodeAsyncMethod(nodeAsyncOperation, null),
        op1 = wrapped(0).then(function (r) { verifyResult(r); }),
        op2 = wrapped(1, 'a').then(function (r) { verifyResult(r, 'a'); }),
        op3 = wrapped(2, 'b', true).then(function (r) { verifyResult(r, 'b', true); }),
        op4 = wrapped(3, 5, 'cat', window).then(function (r) { verifyResult(r, 5, 'cat', window); });

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3, op4]).then(start);
});

asyncTest("The invoked PromiseFactory produced by 'Convert.fromNodeAsyncMethod' is run under the proper context", 3, function () {

    // Create a simple asynchronous operation and wrapper for it.
    function nodeAsyncOperation(expectedContext, callback) {

        ok(this === expectedContext, "The operation was executed under the expected context.");

        setTimeout(function () {

            callback();
        }, 0);
    }

    // Contexts
    var context1 = null,    // This should result in a context of the window...
        context2 = {},
        context3 = { a: "This is a string." };

    // Invoke the factory a few times.
    var op1 = Promises.Convert.fromNodeAsyncMethod(nodeAsyncOperation, context1)(window),
        op2 = Promises.Convert.fromNodeAsyncMethod(nodeAsyncOperation, context2)(context2),
        op3 = Promises.Convert.fromNodeAsyncMethod(nodeAsyncOperation, context3)(context3);

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});

asyncTest("The invoked PromiseFactory produced by 'Convert.fromNodeAsyncMethod' uses the proper 'this' context when no explicit context is provided and the factory is attached to a prototype.", 3, function () {

    // Create a simple asynchronous operation and wrapper for it.
    function nodeAsyncOperation(callback) {

        // Capture the value so that we don't need to do any additional "apply"-ing.
        var context = this;
        setTimeout(function () {

            callback(null, context.val);
        }, 0);
    }

    function testObject(initialValue) {
        this.val = initialValue;
    }

    // Contexts
    var context1 = new testObject(1),
        context2 = new testObject('hello'),
        context3 = new testObject(true);

    // Create a wrapper that has no explicit context, attaching it to the prototype of the object.
    testObject.prototype.getValueAsync = Promises.Convert.fromNodeAsyncMethod(nodeAsyncOperation);

    var op1 = context1.getValueAsync().then(function (v) { ok(v === 1, "The expected value was retrieved."); }),
        op2 = context2.getValueAsync().then(function (v) { ok(v === 'hello', "The expected value was retrieved."); }),
        op3 = context3.getValueAsync().then(function (v) { ok(v === true, "The expected value was retrieved."); });

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});

asyncTest("The invoked PromiseFactory produced by 'Convert.fromNodeAsyncMethod' uses the proper 'this' context when no explicit context is provided and the factory is attached to arbitrary objects.", 3, function () {

    function nodeAsyncMethod(callback) {
        // Capture the value so that we don't need to do any additional "apply"-ing.
        var context = this;
        setTimeout(function () {

            callback(null, context.v.toString());
        }, 0);
    }

    // Contexts
    var context1 = { v: 1 },
        context2 = { v: 'hello' },
        context3 = { v: true };

    // Create a wrapper that has no explicit context, attaching it to the objects.
    getValueAsync = Promises.Convert.fromNodeAsyncMethod(nodeAsyncMethod);
    context1.getValueAsync = getValueAsync;
    context2.getValueAsync = getValueAsync;
    context3.getValueAsync = getValueAsync;

    var op1 = context1.getValueAsync().then(function (v) { ok(v === '1', "The expected value was retrieved."); }),
        op2 = context2.getValueAsync().then(function (v) { ok(v === 'hello', "The expected value was retrieved."); }),
        op3 = context3.getValueAsync().then(function (v) { ok(v === 'true', "The expected value was retrieved."); });

    // When all the operations are done, the test is done.
    Promises.Promise.whenAll([op1, op2, op3]).then(start);
});

//#endregion

//#region Bulk Method Conversion

//#region Node Methods on a Prototype

test("Calling 'Convert.objectNodeAsyncMethods' on a prototype generates the expected methods on that prototype", 15, function () {

    function thingy() { }

    var operations = ['op1', 'op2', 'op3', 'op4', 'op5'],
        suffix = 'AsyncTest';

    for (var i = 0; i < operations.length; i++) {
        thingy.prototype[operations[i]] = function () { };
    }

    // Convert the methods.
    Promises.Convert.objectNodeAsyncMethods(thingy.prototype, operations, suffix);

    for (var i = 0; i < operations.length; i++) {
        ok(operations[i] in thingy.prototype, "Operation '" + operations[i] + "' is defined.")
        ok((operations[i] + suffix) in thingy.prototype, "Promise-generating version of operation '" + operations[i] + "' is defined properly as '" + (operations[i] + suffix) + "'.")
        ok(thingy.prototype[operations[i]] !== thingy.prototype[operations[i] + suffix], "Promise-generating and original version of operation '" + operations[i] + "' different.")
    }
});

test("Calling 'Convert.objectNodeAsyncMethods' on a prototype that does not possess a specified method generates the expected methods on that prototype and an exception", 13, function () {

    function thingy() { }

    var operations = ['op1', 'op2', 'op3', 'op4'],
        suffix = 'AsyncTest';

    for (var i = 0; i < operations.length; i++) {
        thingy.prototype[operations[i]] = function () { };
    }

    // Convert the methods, expecting an exception;
    throws(function () {
        Promises.Convert.objectNodeAsyncMethods(thingy.prototype, operations.concat(['op5']), suffix);
    }, /op5/, "Indicated that an operation on the prototype was not present.");

    for (var i = 0; i < operations.length; i++) {
        ok(operations[i] in thingy.prototype, "Operation '" + operations[i] + "' is defined.")
        ok((operations[i] + suffix) in thingy.prototype, "Promise-generating version of operation '" + operations[i] + "' is defined properly as '" + (operations[i] + suffix) + "'.")
        ok(thingy.prototype[operations[i]] !== thingy.prototype[operations[i] + suffix], "Promise-generating and original version of operation '" + operations[i] + "' different.")
    }
});

test("Calling 'Convert.objectNodeAsyncMethods' on a prototype that already possesses a target method generates the expected methods on that prototype and an exception", 16, function () {

    function thingy() { }

    // Create the duplicate target operation.
    thingy.prototype.op5AsyncTest = function () { };

    var operations = ['op1', 'op2', 'op3', 'op4', 'op5'],
        suffix = 'AsyncTest';

    for (var i = 0; i < operations.length; i++) {
        thingy.prototype[operations[i]] = function () { };
    }

    // Convert the methods, expecting an exception;
    throws(function () {
        Promises.Convert.objectNodeAsyncMethods(thingy.prototype, operations, suffix);
    }, /op5/, "Indicated that a target operation on the prototype was already present.");

    for (var i = 0; i < operations.length; i++) {
        ok(operations[i] in thingy.prototype, "Operation '" + operations[i] + "' is defined.")
        ok((operations[i] + suffix) in thingy.prototype, "Promise-generating version of operation '" + operations[i] + "' is defined properly as '" + (operations[i] + suffix) + "'.")
        ok(thingy.prototype[operations[i]] !== thingy.prototype[operations[i] + suffix], "Promise-generating and original version of operation '" + operations[i] + "' different.")
    }
});

//#endregion

//#region Node Methods on Objects

test("Calling 'Convert.objectNodeAsyncMethods' on an object generates the expected methods on that object and not the prototype", 35, function () {

    function thingy() { }

    var operations = ['op1', 'op2', 'op3', 'op4', 'op5'],
        suffix = 'AsyncTest',
        thingInstance = new thingy(),
        controlThingInstance = new thingy();

    for (var i = 0; i < operations.length; i++) {
        thingInstance[operations[i]] = function () { };
    }

    // Convert the methods.
    Promises.Convert.objectNodeAsyncMethods(thingInstance, operations, suffix);

    // Verify that the prototype is unchanged.
    for (var i = 0; i < operations.length; i++) {
        ok(!(operations[i] in thingy.prototype), "Operation '" + operations[i] + "' is not defined on the prototype.");
        ok(!((operations[i] + suffix) in thingy.prototype), "Promise-generating version of operation '" + operations[i] + "' is not defined on the prototype.");

        // Verify the methods aren't on the other instance.
        ok(!(operations[i] in controlThingInstance), "Operation '" + operations[i] + "' is not defined on the control object.");
        ok(!((operations[i] + suffix) in controlThingInstance), "Promise-generating version of operation '" + operations[i] + "' is not defined on the control object.");

        ok(operations[i] in thingInstance, "Operation '" + operations[i] + "' is defined on the object.");
        ok((operations[i] + suffix) in thingInstance, "Promise-generating version of operation '" + operations[i] + "' is defined properly as '" + (operations[i] + suffix) + "'.");
        ok(thingy.prototype[operations[i]] !== thingInstance[operations[i] + suffix], "Promise-generating and original version of operation '" + operations[i] + "' different.");
    }
});

//#endregion

//#region Synchronous Methods on a Prototype

test("Calling 'Convert.objectSyncMethods' on a prototype generates the expected methods on that prototype", 15, function () {

    function thingy() { }

    var operations = ['op1', 'op2', 'op3', 'op4', 'op5'],
        suffix = 'AsyncTest';

    for (var i = 0; i < operations.length; i++) {
        thingy.prototype[operations[i]] = function () { };
    }

    // Convert the methods.
    Promises.Convert.objectSyncMethods(thingy.prototype, operations, suffix);

    for (var i = 0; i < operations.length; i++) {
        ok(operations[i] in thingy.prototype, "Operation '" + operations[i] + "' is defined.")
        ok((operations[i] + suffix) in thingy.prototype, "Promise-generating version of operation '" + operations[i] + "' is defined properly as '" + (operations[i] + suffix) + "'.")
        ok(thingy.prototype[operations[i]] !== thingy.prototype[operations[i] + suffix], "Promise-generating and original version of operation '" + operations[i] + "' different.")
    }
});

test("Calling 'Convert.objectSyncMethods' on a prototype that does not possess a specified method generates the expected methods on that prototype and an exception", 13, function () {

    function thingy() { }

    var operations = ['op1', 'op2', 'op3', 'op4'],
        suffix = 'AsyncTest';

    for (var i = 0; i < operations.length; i++) {
        thingy.prototype[operations[i]] = function () { };
    }

    // Convert the methods, expecting an exception;
    throws(function () {
        Promises.Convert.objectSyncMethods(thingy.prototype, operations.concat(['op5']), suffix);
    }, /op5/, "Indicated that an operation on the prototype was not present.");

    for (var i = 0; i < operations.length; i++) {
        ok(operations[i] in thingy.prototype, "Operation '" + operations[i] + "' is defined.")
        ok((operations[i] + suffix) in thingy.prototype, "Promise-generating version of operation '" + operations[i] + "' is defined properly as '" + (operations[i] + suffix) + "'.")
        ok(thingy.prototype[operations[i]] !== thingy.prototype[operations[i] + suffix], "Promise-generating and original version of operation '" + operations[i] + "' different.")
    }
});

test("Calling 'Convert.objectSyncMethods' on a prototype that already possesses a target method generates the expected methods on that prototype and an exception", 16, function () {

    function thingy() { }

    // Create the duplicate target operation.
    thingy.prototype.op5AsyncTest = function () { };

    var operations = ['op1', 'op2', 'op3', 'op4', 'op5'],
        suffix = 'AsyncTest';

    for (var i = 0; i < operations.length; i++) {
        thingy.prototype[operations[i]] = function () { };
    }

    // Convert the methods, expecting an exception;
    throws(function () {
        Promises.Convert.objectSyncMethods(thingy.prototype, operations, suffix);
    }, /op5/, "Indicated that a target operation on the prototype was already present.");

    for (var i = 0; i < operations.length; i++) {
        ok(operations[i] in thingy.prototype, "Operation '" + operations[i] + "' is defined.")
        ok((operations[i] + suffix) in thingy.prototype, "Promise-generating version of operation '" + operations[i] + "' is defined properly as '" + (operations[i] + suffix) + "'.")
        ok(thingy.prototype[operations[i]] !== thingy.prototype[operations[i] + suffix], "Promise-generating and original version of operation '" + operations[i] + "' different.")
    }
});

//#endregion

//#region Synchronous Methods on Objects

test("Calling 'Convert.objectSyncMethods' on an object generates the expected methods on that object and not the prototype", 35, function () {

    function thingy() { }

    var operations = ['op1', 'op2', 'op3', 'op4', 'op5'],
        suffix = 'AsyncTest',
        thingInstance = new thingy(),
        controlThingInstance = new thingy();

    for (var i = 0; i < operations.length; i++) {
        thingInstance[operations[i]] = function () { };
    }

    // Convert the methods.
    Promises.Convert.objectSyncMethods(thingInstance, operations, suffix);

    // Verify that the prototype is unchanged.
    for (var i = 0; i < operations.length; i++) {
        ok(!(operations[i] in thingy.prototype), "Operation '" + operations[i] + "' is not defined on the prototype.");
        ok(!((operations[i] + suffix) in thingy.prototype), "Promise-generating version of operation '" + operations[i] + "' is not defined on the prototype.");

        // Verify the methods aren't on the other instance.
        ok(!(operations[i] in controlThingInstance), "Operation '" + operations[i] + "' is not defined on the control object.");
        ok(!((operations[i] + suffix) in controlThingInstance), "Promise-generating version of operation '" + operations[i] + "' is not defined on the control object.");

        ok(operations[i] in thingInstance, "Operation '" + operations[i] + "' is defined on the object.");
        ok((operations[i] + suffix) in thingInstance, "Promise-generating version of operation '" + operations[i] + "' is defined properly as '" + (operations[i] + suffix) + "'.");
        ok(thingy.prototype[operations[i]] !== thingInstance[operations[i] + suffix], "Promise-generating and original version of operation '" + operations[i] + "' different.");
    }
});

//#endregion

//#endregion

//#endregion